#!/usr/bin/python
# -*- coding: utf-8 -*-

import time
import ram as memoryHW
import cpuDef
import constante as cons
from threading import Thread


class TemplateGraphicEngine(object):
	""" Destiné à ne pas être utilisé directement. Uniquement là pour éviter
	la redondance de code dans les interfaces graphique.
	Il doit être dérivé UNIQUEMENT 
	TODO: define private and public attributes. Define meta-func for bind_input"""
	def __init__(self):
		self.coord = (divmod(e, cons.SCR_HEIGHT * cons.SCR_HEIGHT) \
					for e in xrange(cons.SCR_HEIGHT * cons.SCR_HEIGHT))
		self.initialize_hardware()
		self.hw.powerOn()
	
	def generator_coord(self):
		""" Generateur utilisé pour parcourir les cases de l'écran """
		for e in xrange((cons.SCR_HEIGHT) * (cons.SCR_WIDTH)):
			res = divmod(e, cons.SCR_WIDTH)
			yield res[1], res[0]
	
	def initialize_hardware(self):
		self.hw = Machine()
		self.bus_input = self.hw.keyboard
	
	def draw(self, clear, refresh, draw_BLACK, draw_WHITE, bind_key):
		""" Le rafraichissement de l'écran est dissocié de celui du CPU.
		Chaque arguement DOIT être une fonction"""
		pre_tick = time.time()
		while cons.IS_RUNNING:
			clear()
			bind_key()
			current_tick = time.time()
			if time.time() - pre_tick > cons.FPS:
				for x, y in self.generator_coord():
					if self.hw.bus_graphic[x,y] == cons.WHITE:
						draw_WHITE(x,y)
					elif self.hw.bus_graphic[x,y] == cons.BLACK:
						draw_BLACK(x,y)
				refresh()
			else:
				time.sleep(cons.FPS - (time.time() - pre_tick))


class Machine(object):
	def __init__(self):
		""" Represente la machine chip8, comme un pc classique.
		Pour démarrer la boucle infinie, il faut le démarrer via la fonction powerOn()"""
		self.ram = memoryHW.Ram()
		self.load_file(cons.ROM)
		self.bus_graphic = dict()
		self.initializeBusGraphic()
		self.keyboard = [0 for e in xrange(0xF)]
		self.cpu = Cpu(self.ram, self.bus_graphic, self.keyboard)
	
	def initializeBusGraphic(self):
		for x in xrange(cons.SCR_WIDTH+1):
			for y in xrange(cons.SCR_HEIGHT+1):
				self.bus_graphic[x,y] = cons.BLACK
	
	def load_file(self, data):
		with open(data, 'rb') as f:
			self.ram.inject_data(f)
	
	def powerOn(self):
		self.thread = Thread(target=self.cpu.loop)
		self.thread.start()


class Cpu(object):
	def __init__(self, bus_ram, bus_graphic, keyboard):
		""" Represent the CPU. Bind to ram, keyboard and screen """
		self.setMemory(bus_ram)
		self.setRegister()
		self.setStack()
		self.setClock()
		self.setGMemory(bus_graphic)
		self.setInput(keyboard)
		# Initialize the dict for mask, signature and function for each opcode
		self.opcodeSign = cpuDef.getOpcodeSignature()
	
	def setMemory(self, mem):
		self.br = mem # Bus Ram
		self.pointRam = 512
	
	def setGMemory(self, mem):
		self.bg = mem # Bus Graphic
	
	def setRegister(self):
		self.register = [0 for i in xrange(16)]
		self.pointReg = 0
	
	def setStack(self):
		self.stack = [0 for i in xrange(16)]
		self.pointStack = 0
	
	def setClock(self):
		self.clockCPU = 0
		self.clockSound = 0
	
	def setInput(self, keyboard):
		self.bus_input = keyboard
	
	
	def loop(self):
		""" Infinite loop. Has his self-thread. Time delay represent frequency NHz = 1/N sec """
		pre_tick = time.time()
		while cons.IS_RUNNING:
			current_tick = time.time()
			if time.time() - pre_tick > cons.FREQUENCY:
				self.executeOpcode(self.getOpcode())
				pre_tick = time.time()
			else:
				time.sleep(cons.FREQUENCY - (time.time() - pre_tick))
	
	
	def getOpcode(self):
		# Retourne les deux cases en mémoires contigues, qui sont selectionnés par le pointeur
		# Chaque case fait 1bit, et les opcodes font 2bits
		return (self.br[self.pointRam]<<8) + self.br[self.pointRam+1]
		
	def getOpcodeFunction(self, opcode):
		# Retourne la fonction associé à l'opcode inconnu envoyé.
		# L'opcode est d'abord filtré par les masques et est ensuite comparés
		# à la signature unique de chaque opcode.
		for sign in self.opcodeSign:
			opcodeID = sign[0] & opcode
			if opcodeID == sign[1]:
				return sign[2]
		
	def executeOpcode(self, opcode):
		# Les bits sont de type big-endian
		bit3 = (opcode & 0x0F00)>>8
		bit2 = (opcode & 0x00F0)>>4
		bit1 = (opcode & 0x000F)
		# Récupère la fonction nécéssaire à l'opcode pointé
		opcodeFunc = self.getOpcodeFunction(opcode)
		
		# DEBUG: affiche la fonction en cours
		#print(opcodeFunc.__doc__, str(bit1) + str(bit2) + str(bit3))
		opcodeFunc(self, bit1, bit2, bit3)
		# Pour continuer le programme, on saute deux cases dans la mémoire
		self.pointRam += 2
	
	
	def decreaseClock(self):
		# Clock for timing
		if self.clockCPU > 0:
			self.clockCPU -= 1
		if self.clockSound > 0:
			self.clockSound -= 1


if __name__ == "__main__":
	import sys
	cons.ROM = sys.argv[1]
	m = Machine()
	m.powerOn()